package se392.ateam2006.meetingmgmt;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import javax.ejb.CreateException;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.GregorianCalendar;

import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import se392.ateam2006.authentication.GroupEntity;
import se392.ateam2006.exceptions.InvalidValueException;
import se392.ateam2006.meetingmgmt.meeting.*;
import se392.ateam2006.resourcemgmt.ResourceMgmtLocal;
import se392.ateam2006.resourcemgmt.participant.ParticipationEntity;
import se392.ateam2006.resourcemgmt.participant.exclusionset.ExclusionRangeEntity;
import se392.ateam2006.resourcemgmt.room.RoomEntity;
import se392.ateam2006.user.UserEntity;
import se392.ateam2006.user.UserMgmtLocal;
import se392.ateam2006.exceptions.MethodNotImplementedException;

/**
 * A stateless EJB to handle all operations that deal with meetings - create/delete metting etc
 * @author Ateam (Matthew Bennett, Claire Melton, Shingai Manyiwa, John Adderley)
 * @version 25/03/07
 */
@Stateless
@PermitAll
public class MeetingMgmtBean implements Serializable, MeetingMgmtRemote, MeetingMgmtLocal {

    @PersistenceContext
    private EntityManager em;
    private int defaultCutOffPeriod = 1;
    private static GregorianCalendar defaultWorkingDayStart;
    private static GregorianCalendar defaultWorkingDayEnd;
    private String defaultWorkingWeekDays = "monday tuesday wednesday thursday friday";
    private GregorianCalendar defaultMeetingStartTimePeriod;
    private GregorianCalendar defaultMeetingEndTimePeriod;
    private int defaultOffSet = 10;
    
    @EJB
    private ResourceMgmtLocal resources;
    @EJB
    private UserMgmtLocal users;
    

    /**
     * Default constructor required as this is an EJB
     */
    public MeetingMgmtBean() {
    }

    public GregorianCalendar getDefaultWorkingDayStart() {
        return defaultWorkingDayStart;
    }

    public void setDefaultWorkingDayStart(GregorianCalendar aDefaultWorkingDayStart) {
        defaultWorkingDayStart = aDefaultWorkingDayStart;
    }

    public GregorianCalendar getDefaultWorkingDayEnd() {
        return defaultWorkingDayEnd;
    }

    public void setDefaultWorkingDayEnd(GregorianCalendar aDefaultWorkingDayEnd) {
        defaultWorkingDayEnd = aDefaultWorkingDayEnd;
    }

    public String getDefaultWorkingWeekDays() {
        return defaultWorkingWeekDays;
    }

    public void setDefaultWorkingWeekDays(String aDefaultWorkingWeekDays) {
        defaultWorkingWeekDays = aDefaultWorkingWeekDays;
    }

    public GregorianCalendar getDefaultMeetingStartTimePeriod() {
        return defaultMeetingStartTimePeriod;
    }

    public void setDefaultMeetingStartTimePeriod(GregorianCalendar aDefaultMeetingStartTimePeriod) {
        defaultMeetingStartTimePeriod = aDefaultMeetingStartTimePeriod;
    }

    public GregorianCalendar getDefaultMeetingEndTimePeriod() {
        return defaultMeetingEndTimePeriod;
    }

    public void setDefaultMeetingEndTimePeriod(GregorianCalendar aDefaultMeetingEndTimePeriod) {
        defaultMeetingEndTimePeriod = aDefaultMeetingEndTimePeriod;
    }

    public int getDefaultOffSet() {
        return defaultOffSet;
    }
    
    public int getDurationInMins(Duration duration){
        return duration.getTimeInMinutes();
    }

    public void setDefaultOffSet(int aDefaultOffSet) {
        defaultOffSet = aDefaultOffSet;
    }

    /**
     * Create a new MeetingEntity and persist it
     * @param initiator - the UserEntity who initiated the meeting
     * @param id - a String id for the meeting
     * @param title - a String title for the meeting
     * @param description - a String description of the meeting
     * @param startDateRange - a GregorianCalendar defining the start date/time of the range that
     *      the meeting may exist in
     * @param endDateRange - a GregorianCalendar defining the end date/time of the range that
     *      the meeting may exist in
     * @param duration - a Duration defining the length of the meeting
     * @param requestedRoomSize - int, the number of people we need to accomodate
     * @param equipment - String, any equipment needed for the meeting
     * @param room - the RoomEntity where this meeting will take place
     * @throws CreateException
     */
//    @RolesAllowed("AUTHORIZED_ADMIN")
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public boolean createMeeting(UserEntity initiator, String id, String title, String description,
            GregorianCalendar startDateRange, GregorianCalendar endDateRange,
            Duration duration, int requestedRoomSize, String equipment, RoomEntity room) throws CreateException {

        em.flush();

        // Check if the meeting already exists
        MeetingEntity ourbean = (MeetingEntity) em.find(MeetingEntity.class, id);
        if (ourbean == null) {

            // If it doesn't exist, create it
            Duration dur = em.find(Duration.class, duration.getID());
            if(dur == null) {
                em.persist(duration);
            }else {
                em.merge(duration);
            }
            em.merge(initiator);
            MeetingEntity meeting = null;
            if (room != null) {
                em.merge(room);
            }
            if (null == startDateRange) {                
                throw new CreateException("The start date range has not been specified.");
            } else if (null == endDateRange) {
                throw new CreateException("The end date range has not been specified.");
            } else if (null == duration) {
                throw new CreateException("Please specify a valid duration");
            } else if (null == initiator || initiator.equals("")) { //TODO: make this make sense
                throw new CreateException("Meeting User not valid");
            } else if (null == id || id.equals("")) {
                throw new CreateException("Meeting PK (ID) not valid");
            } else if (startDateRange.equals(endDateRange)) {
                throw new CreateException("Start Date is the Same as the End Date");
            } else if (startDateRange.getTimeInMillis() > endDateRange.getTimeInMillis()) {
                throw new CreateException("Start Date is the after the End Date");
            } else if ((endDateRange.getTimeInMillis() - startDateRange.getTimeInMillis()) < duration.getTimeInMillis()) {
                throw new CreateException("The duration is longer than the meeting range");
            } else if ((endDateRange.getTimeInMillis() - startDateRange.getTimeInMillis()) < (1 * 24 * 60 * 60 * 1000)) { //TODO: first number should be cutOffPeriod!
                throw new CreateException("The meeting has a range which is less than the cut off period. Cut-Off Period: 1");//TODO: make this relative to above
            } else if (room != null && room.getCapacity() < requestedRoomSize) {
                throw new CreateException("Room is too small for meeting");
            } else {
                List<GroupEntity> permissions = initiator.getPermissions();
                boolean hasPermission = false;
                System.err.println("CAZ: list permissions " + permissions);
                for (GroupEntity ge : permissions) {
                    System.err.println("CAZ: permission " + ge.getGroupid());
                    //This may look like it is always going to evaluate to be true
                    //but will only be true if the user has permission to book the room
                    //or the room is not set.
                    if (room != null) {
                        if (room.getOwner().equalsIgnoreCase(ge.getGroupid())) {
                            hasPermission = true;
                        }
                        if (room.getRoomID().equalsIgnoreCase("blank")) {
                            hasPermission = true;
                        }
                    } else {
                        hasPermission = true;
                    }
                }
                if (hasPermission == false) {
                    throw new CreateException("User doesn't have permission to book room");
                }
            }


            meeting = new MeetingEntity(initiator, id, title, description, startDateRange, endDateRange,
                    duration, requestedRoomSize, equipment, room);

            em.persist(meeting);
            System.out.println("MeetingMgmt.createMeeting() has successfully added meeting: " + id + " (initiator: " + initiator + ")");
            String caz = duration.toString();
            return true;
        } else {
            throw new CreateException("The meeting with id: \"" + id + "\" already exists.");
        }
    }
    

    /**
     * Create a new Duration and persist it
     * @param days - int, number of days in the duration
     * @param hours - int, the number of hours in the duration
     * @param mins - int, the number of minutes in the duration
     * @throws CreateException
     */
    public Duration createDuration(int days, int hours, int mins) throws CreateException {
        Duration duration = null;
        if (mins <= 10 && hours < 1 && days < 1) {
            throw new CreateException("Please specify a valid duration");
        }
        try {
            duration = new Duration(days, hours, mins);
        } catch (InvalidValueException ex) {
            ex.printStackTrace();
        }
        em.persist(duration);
        return duration;
    }

    /**
     * Find a duration based on its ID
     * @param id - long, the ID of the duration to find
     * @return Duration - the found duration (or null if not found)
     */
    public Duration findDurationByID(long id) {
        return em.find(Duration.class, id);
    }

    /**
     * Find a meeting based on its ID
     * @param id - String, the ID of the meeting to find
     * @return MeetingEntity - the foud MeetingEntity (or null if not found)
     */
    public MeetingEntity findMeetingById(String id) {
        return em.find(MeetingEntity.class, id);
    }

    /**
     * Delete a meeting
     * @param me - the MeetingEntity to delete
     */
    public void deleteMeeting(MeetingEntity me) {
        deleteMeetingById(me.getId());
    }

    /**
     * Delete a meeting based on its ID
     * @param id - String, the ID of the meeting to delete
     */
    public boolean deleteMeetingById(String id) {
        em.flush();
        List participations = null;
        List exclusions = null;
        Query q = null;

        try {
            //DEPENDANCIES:
            //  Todo: If you can get cascading deletes to work here, do!
            //  Todo: Also, there should be some find x by y methods for this!
            // -- PARTICIPANTS

            q = em.createQuery("Select p from ParticipationEntity p " +
                    "where p.meeting.id = '" + id + "'");
            participations = q.getResultList();
            for (Object item : participations) {
                ParticipationEntity p = (ParticipationEntity) item;
                resources.removeParticipation(p);
            }

            // -- EXCLUSIONS
            q = em.createQuery("Select p from ExclusionRangeEntity p " +
                    "where p.meetingEnt.id = '" + id + "'");
            exclusions = q.getResultList();
            for (Object item : exclusions) {
                ExclusionRangeEntity p = (ExclusionRangeEntity) item;
                resources.removePreAndExSet(p);
            }


            //MEETING
            MeetingEntity me = em.find(MeetingEntity.class, id);
            if (me != null) {
                em.remove(me);
            }
            q = em.createQuery("DELETE FROM MeetingEntity p WHERE p.id = '" + id + "'");
            q.executeUpdate();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Set an existing meeting as booked
     * @param meetingId - String, the ID of the meeting to update
     * @param booked - boolean, is the meeting booked?
     */
    public void setMeetingAsBooked(String meetingId, boolean booked) {
        MeetingEntity me = em.find(MeetingEntity.class, meetingId);
        me.setBooked(booked);
        em.merge(me);
    }

    /**
     * Get all of the meetings who share the specified initiator
     * @param user - the UserEntity to look for
     * @return Collection - a collection of all meetings that were initiated by 'user'
     */
    public Collection findMeetingsByInitiator(UserEntity user) {
        em.flush();
        Query q = em.createQuery("SELECT p FROM MeetingEntity p WHERE p.initiator.email = '" + user.getEmail() + "'");
        //Query q = em.createNativeQuery("SELECT * FROM ADMIN.MEETINGENTITY WHERE INITIATOR_EMAIL = '" + user.getEmail() + "'");
        List list = q.getResultList();

        return list;
    }

    /**
     * Get all of the meetings in the database
     * @return Collection - all of the meetings in the database
     */
    public Collection getAllMeetings() {
        em.flush();

        Query q = em.createQuery("SELECT p FROM MeetingEntity p");
        return q.getResultList();
    }

    /**
     * Find a ParticipationEntity based on its ID (which is the email address of the user)
     * @param id - String, the ID of the participation entity to find
     * @return ParticipationEntity - the found ParticipationEntity
     */
    
    private ParticipationEntity findParticipationEntityByEmail(String id) {

        Query q = em.createQuery("SELECT p FROM ParticipationEntity WHERE id.email = '" + id + "'");
        return (ParticipationEntity) (q.getSingleResult());
    }

    /**
     * Get all of the booked meetings in the database
     * @return Collection - all of the booked meetings
     */
    public Collection getBookedMeetings() {

        Query q = em.createQuery("SELECT p FROM MeetingEntity p where p.booked = true");
        return q.getResultList();
    }

    /**
     * Get the number of meetings in the database
     * @return int - the number of meetings
     */
    public int getMeetingCount() {

        Query q = em.createQuery("SELECT p FROM MeetingEntity p");
        return q.getResultList().size();
    }

    /**
     * Remove all of the meetings from the database
     */
    public void removeAllMeetings() {

        Collection meets = getAllMeetings();
        for (Object me : meets) {
            this.deleteMeeting((MeetingEntity) me);
        }

    }

    /**
     * Find all meetings for a specified user
     * @param userId - String, the ID of the user who's meetings we want to find
     * @return Collection - all of the meetings that the user will be attending
     */
    public Collection<MeetingEntity> findMeetings(String userId) {
        throw new MethodNotImplementedException("MeetingMgmtBean.findMeetings()");
    }

    public void removeAllEquipment() {
        throw new MethodNotImplementedException("MeetingMgmtBean.removeAllEquipment()");
    }

    public boolean removeEquipmentRequirementsForMeeting(String meetingID) {
        MeetingEntity theMeeting = em.find(MeetingEntity.class, meetingID);
        if (theMeeting != null) {
            theMeeting.setEquipment(null);
            return true;
        }
        return false;
    }

    /**
     * Used for adding equipment constraint seems to have params that not needed
     * might need to create an equipment object
     * This doesn't work yet it needs the string building up and gets a null pointer exception
     * @param userId
     * @param meetingId
     * @param equipmentType
     * @param number
     * @return
     */
    public boolean addEquipmentConstraint(String userId, String meetingId, String equipmentType, int number) {
        //boolean canGetEquip = false;
        MeetingEntity theMeeting = em.find(MeetingEntity.class, meetingId);
        if (theMeeting != null) {
            UserEntity theUser = users.getUserByID(userId);
            ParticipationEntity pet = resources.getParticipant(theUser, theMeeting);
            // check user exists and is active
            if (theUser != null && pet.isActive()) {
                for (int i = 0; i < number; i++) {
                    theMeeting.setEquipment(theMeeting.getEquipment() + "," + equipmentType);
                }
                return true;
            }
        }
        return false;

    }

    public boolean bookTheMeeting(String meetingId, String venue, String startDate) {
        MeetingEntity theMeeting = em.find(MeetingEntity.class, meetingId);
        if (theMeeting != null) {
            RoomEntity theRoom = resources.findRoomByID(venue);
            SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd kk:mm");
            GregorianCalendar startDateCal = new GregorianCalendar();
            startDateCal.set(Calendar.SECOND,0);
            try {
                startDateCal.setTime(format.parse(startDate));
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            theMeeting.setStartDate(startDateCal);
            GregorianCalendar endDate = new GregorianCalendar();
            endDate.set(Calendar.SECOND, 0);
            long theDuration = theMeeting.getDuration().getTimeInMillis();
            long startTimeInMills = startDateCal.getTimeInMillis();
            endDate.setTimeInMillis(theDuration + startTimeInMills);
            theMeeting.setEndDate(endDate);
            theMeeting.setRoom(theRoom);
            theMeeting.setBooked(true);
            return true;
        }
        return false;
    }

    /**
     * Method to remove exclusion sets based on which meeting they belong to
     * Note: This method probably could be done better and might not want to be here
     * ccf4
     * @param meetingID
     * @return
     */
    public boolean removeExclusionSetsByMeetingId(String meetingID) {
        MeetingEntity meeting = em.find(MeetingEntity.class, meetingID);
        if (meeting != null) {
            Collection<ExclusionRangeEntity> exclusions = resources.getAllExclusionsAndPreferences();
            for (ExclusionRangeEntity ex : exclusions) {
                if ((ex.isPreferenceSet() == false) && ex.getMeeting().equals(meeting)) {
                    resources.removePreAndExSet(ex);
                }
            }
            return true;
        }
        return false;
    }

    public void setMeetingStartPeriods(GregorianCalendar startDate, GregorianCalendar endDate, int offset) {
        setDefaultOffSet(offset);
        setDefaultMeetingEndTimePeriod(endDate);
        setDefaultMeetingStartTimePeriod(startDate);
    }

    public Collection<MeetingCandidate> resolveMeetingTime(String meetingId) {
        MeetingTimeResolver mtr = new MeetingTimeResolver(resources, defaultWorkingWeekDays, this.getBookedMeetings(), defaultWorkingDayStart, defaultWorkingDayEnd);
        Duration duration = this.findMeetingById(meetingId).getDuration();
        int durationInMinutes = calcDuration(duration);
        MeetingEntity meeting = this.findMeetingById(meetingId);
        return mtr.resolveTimes(meeting, defaultOffSet);
    }

    private int calcDuration(Duration duration) {
        int durationInMinutes = duration.getDays() * 24 * 60;
        durationInMinutes += duration.getHours() * 60;
        durationInMinutes += duration.getMins();
        return durationInMinutes;
    }

    public String getEquipmentForMeeting(String userId, String meetingId) {
        MeetingEntity meeting = em.find(MeetingEntity.class, meetingId);
        return meeting.getEquipment();
    }

    public void setWorkingDayEnd(GregorianCalendar date) {
        this.defaultWorkingDayEnd = date;
    }

    public void setWorkingDayStart(GregorianCalendar date) {
        this.defaultWorkingDayStart = date;
    }

    public void setWorkingDays(String days) {
        this.defaultWorkingWeekDays = days;
    }

    public int getDefaultCutOffPeriod() {
        return defaultCutOffPeriod;
    }

    public void setDefaultCutOffPeriod(int defaultCutOffPeriod) {
        this.defaultCutOffPeriod = defaultCutOffPeriod;
    }

    
}
